使用spring boot开发Java web应用

项目地址为:https://github.com/flowerlake/spring-blog-website

  1. spring系列框架的理解
  2. 与前端交互的方式
  3. 后台如何接受并处理请求
  4. spring boot的一些基本概念及项目框架的搭建
  5. 对orm框架:mybatis+mysql的理解及应用
  6. 如何在spring boot项目中集成nosql。例MongoDB
  7. 文件流的处理
  8. 使用docker部署项目
  9. web设计安全及编码规范
  10. 具体编码过程中的一些细节
  11. 总结

spring mvc框架及一些基本概念

Spring作为Java系的全栈(full-stack)开发框架,当然也对Web开发有着非常好的支持。Spring的Web MVC框架能够使开发者非常容易的开发Web应用,同时能够无缝享受到Spring本身的诸多好处——IoC容器、AOP编程等等。它对HTTP请求处理的模型如下图(图来自天码营):
Spring MVC请求处理流程
上图中,web服务器接收到的请求会经过DispatcherServlet的分发,经过一系列的 interceptor 进行预处理,在这里数据的预处理顺序与interceptor的顺序有关,
之后,将请求交给controller进行处理,返回请求(response)时同样会经过一系列的 interceptor 进行后处理,这就是 interceptor 的预处理和后处理功能。
更多关于interceptor的内容查看 #interceptor。

基本概念

  • ORM(object relation mapping),顾名思义是 对象关系映射。
    ORM是一种以面向对象的方式来进行数据库操作的技术。Web开发中常用的语言,都会有对应的ORM框架。而MyBatis就是Java开发中一种常用ORM框架。
    ORM框架:hibernate和mybatis;spring data jpa是 Spring Data的子模块 hibernate 作为ORM实现。
    面向对象致力于解决计算机逻辑问题,而关系模型致力于解决数据的高效存取问题。

  • IoC(Inversion of Control,控制反转)
    使用Spring的@Component标注将QunarBookingService注册进Spring的Context,这样它就可以被注入到需要它的地方!相应地,创建QunarBookingService实例的责任也交给了Spring。

  • AOP(Aspect Oriented Programming,AOP)
    面向切面编程(Aspect Oriented Programming,AOP)其实就是一种关注点分离的技术,在软件工程领域一度是非常火的研究领域。
    我们软件开发时经常提一个词叫做“业务逻辑”或者“业务功能”,我们的代码主要就是实现某种特定的业务逻辑。但是我们往往不能专注于业务逻辑,
    比如我们写业务逻辑代码的同时,还要写事务管理、缓存、日志等等通用化的功能,而且每个业务功能都要和这些业务功能混在一起,痛苦!
    所以,为了将业务功能的关注点和通用化功能的关注点分离开来,就出现了AOP技术。这些通用化功能的代码实现,对应的就是我们说的切面(Aspect)。

与前端交互的方式

前端与后端的交互,本质上只有4种交互方式,即GET、POST、PUT、DELETE,一般来说仅涉及到GET、POST两种方式,剩下的就是前端的渲染。
对于前端渲染也有2种方式:一种是通过模板引擎对网页进行渲染,这种方式本质上是在服务端对网页进行渲染,即向事先写好的HTML模板中填充内容,然后将填充完的内容返回到前端页面;另一种方式就是通过ajax的方式进行数据请求,彻底将前后端分开,这种方式的处理方式是浏览器接收后台返回的原始HTML页面,在接收到页面请求的一瞬间就执行ajax部分的代码,ajax发出GET请求,得到数据,使用JavaScript(当然现在更方便的是使用jQuery)来对前端页面进行渲染。这两种方式各有优劣,但是在大公司里面前后端分工很细的情况下,大概率都会选择第2种方式进行设计。

基本的交互方式就不说了,网上有很多理解HTTP的文章,主要说一下前端渲染的方式:

1、模板引擎
在Java的开发web开发中,有很多模板引擎,比如大名鼎鼎的thymeleaf,模板之间没有什么特殊的差别,目前还没有碰到thymeleaf不能解决的渲染情况。
使用thymeleaf进行HTML的渲染,好处就是代码比较简单、开发比较便捷,缺点就是不符合前后端完全分离的思想,页面在服务端渲染加重了服务器的计算负担。

2、使用ajax进行请求渲染
这个需要在前端页面不少前端页面渲染的代码。

注意:视频网站是怎样的处理方式,暂时还没有研究。

后台如何接受并处理请求

首先服务器接收到tcp连接(HTTP)后,与服务器建立连接,服务器收到HTTP请求,将其转化为对应的端口如80端口,nginx监听80端口,将80端口的数据转发到对应的处理服务,即后台服务程序spring-blog-website的8080端口。该应用程序(spring boot)集成了Tomcat动态数据服务器,8080端口 收到nginx转发过来的请求后,实际上是转发给Tomcat服务器,然后Tomcat把请求转发给后台服务程序,后台服务程序根据controller处理对应的请求。

后台接收到请求后,都是在围绕着一件事在做——即对数据的处理。

对数据的处理虽然听上去很简单,但是这里面又涉及到很多附带的点,比如数据的校验、加密、基本处理等,然后就涉及到数据的增删改查以及数据的持久化,整个后台如何处理数据是核心。即网站设计时要考虑的很多方面也都是围绕着数据进行的,如数据结构的设计、什么类型的数据选择对应的数据库解决方案,比如本文中使用较多的文本类型的数据,那就可以选择MongoDB作为后台数据库。

后台应用程序干完上述的事情之后,就可以把从数据库中得到的数据进行一番操作之后返回给浏览器,浏览器接收到响应之后对数据页面进行渲染。

nginx的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server {
listen 80;
charset utf-8;
access_log off;

location / {
proxy_pass http://spring-blog-website:8080;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

client_max_body_size 100M;
}

location /static {
access_log off;
expires 30d;

alias /spring-blog-website/static;
}
}

spring boot的一些基本概念及项目框架的搭建

新建一个项目的时候,第一要熟悉这个框架的结构,才能更好的梳理出整个项目的大致框架,等到建好这个项目的大框架之后,就可以丰富内部空间了。第一次接触spring的框架难免有些懵逼,不懂这个框架的结构是什么样的,现在来简单了解一下。在spring的官网首页上,有这么一张图 Spring Framework 5:

在上图中,我们可以看到spring框架现在构建基石是spring boot2.0,然后在网站下有spring boot2.0的一些官方文档:

如果要新建一个spring boot的项目,主要有两种方式:

  • 一种是在本地IDEA中选择新建spring initialize项目,选择default,然后选择需要的组件
  • 一种是通过spring提供的https://start.spring.io/ 网站初始化项目所需要的组件。

新建完项目之后,得到一个基本的spring boot项目框架,项目文件夹resource下有一个application.properties(当然,可以有多个properties文件,通过在application.peoperties中定义使用其中某个配置文件,这一点可以用于本地开发和部署环境存在些许差异时方便的进行切换。)

除此之外,还需要进一步搭建web应用程序的框架。一般来说,在web项目中,通常要包括几个方面,controller层、service层、model层、dao层。

  • controller层写class,写具体的流程控制,接收service层获得的数据,然后传递给前端页面。
  • service层写interface,写一些业务逻辑,不同于 mapper,mapper主要是写数据库操作的逻辑代码,service 主要写针对业务的一些从数据库的代码接口。
  • model层写Bean,设计每一块业务对应的数据项,并提供一些方法。
  • dao层写mybatis对应的一些mapper,即操作数据对象与sql语句进行对应,当然,也可以写nosql。

最后,这个项目的基本框架就搭起来了,接下来就是向里面填东西了。本文设计的Java web不涉及传统的jsp开发,前端界面的渲染是通过ajax和模板引擎(thymeleaf)共同完成的。网上有很多文章还都是旧的技术,从jsp这些说起,感觉太落伍了,就琢磨出了这套开发模式。但是也要记住spring mvc也是在servelet基础上封装的,熟悉一下servelet的技术还是没有坏处的,但是jsp就不推荐了。

对orm框架:mybatis+mysql的理解及应用

ORM(object relation mapping),顾名思义是 对象关系映射。
ORM是一种以面向对象的方式来进行数据库操作的技术。Web开发中常用的语言,都会有对应的ORM框架。而MyBatis就是Java开发中一种常用ORM框架。现在开发中主流的ORM框架有hibernate和mybatis,而Spring data jpa是 Spring Data的子模块 hibernate 作为ORM实现。

面向对象致力于解决计算机逻辑问题,而关系模型致力于解决数据的高效存取问题。

MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架,几乎避免了所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以对配置和原生Map使用简单的 XML 或标注,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。简单地理解,你可以认为MyBatis将SQL语句,以及SQL返回结果到Java对象的映射,都放到了一个易于配置的XML文件里了,你的Java代码就会变得异常简单。在xml文件中,通过定义mapper对象,可以把Mybatis的查询语句映射到SQL语句上。

当然,除了XML,MyBatis同时也支持基于标注的方式,但是功能上会有一些限制。总体来说,我们推荐使用XML方式,一些简单的SQL使用标注会更方便一些。

在spring boot项目中,整合Mybatis主要有以下两种方式:

  • Mybatis提供的第一种映射方法:使用mapper进行映射。mapper接口,用来定义数据库的查询、增加、删除等操作;

    1
    2
    3
    4
    5
    <mapper namespace="org.flowerlake.blogwebsite.dao.UserMapper">
    <insert id="insert" parameterType="org.flowerlake.blogwebsite.model.User">
    INSERT INTO `t_user`(`username`, `password`) VALUES (#{username}, #{password})
    </insert>
    </mapper>
  • Mybatis提供的第二种映射方法,直接在数据库操作类中使用@插入注解。

    1
    2
    @("SELECT * FROM t_user WHERE username = #{username}")
    List<User> findByUsername(@Param("username") String username);

以上两种方法都需要在接口类上加上@Mapper注解。

使用spring-boot提供的application.properties文件将mybatis整合到spring-boot项目中

1
2
3
4
5
6
7
spring.datasource.url=jdbc:mysql://localhost:3306/java_web?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
spring.datasource.username=username
spring.datasource.password=password

#这种方式需要自己在resources目录下创建mapper目录然后存放xml
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=org.flowerlake.blogwebsite.model

使用orm框架能否有效控制SQL注入漏洞的产生呢?首先,我们要看一下mybatis的源码:

如何在spring boot项目中集成nosql

本文以MongoDB为例讲解spring boot继承nosql。MongoDB的和jpa有点类似,都是通过名称注解。在spring boot项目中集成MongoDB数据库,只需要以下几个步骤:

  1. 在application.properties中添加MongoDB的连接信息,当然在实际项目中,要添加数据库用户信息(用户名+密码等)

    1
    spring.data.mongodb.uri=mongodb://localhost:27017/java_web
  2. 定义数据结构,即model类,在类前加上@Document注解。

    1
    2
    3
    4
    5
    6
    @Document(collection = "articles")
    public class Article {
    @Id
    private ObjectId _id;
    private String title;
    }
  3. 定义一个增删改查的接口,提供增删改查方法的定义,该接口需要继承自MongoRepository。

    1
    2
    3
    4
    5
    6
    7
    @Repository
    public interface ArticleRepository extends MongoRepository<Article, String> {
    List<Article> findByAuthor(String author);
    Article findByEnTitle(String enTitle);
    Article findByTitle(String title);
    Page<Article> findAll(Pageable pageable);
    }
  4. 写具体的服务逻辑,在service中新建service接口,然后实现它、当然直接写具体的服务类也是没有问题的,面对业务需求灵活改变即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Service
    public class ArticleServiceImpl implements ArticleService {
    @Resource
    private MongoTemplate mongoTemplate;
    @Resource
    private ArticleRepository articleRepository;
    @Override
    public List<Article> findByAuthor(String author) {
    return articleRepository.findByAuthor(author);
    }
    @Override
    public boolean updateArticle(Article article) {
    try {
    Query query = new Query(new Criteria("id").is(article.getEnTitle()));
    Update update = new Update();
    update.set("author", article.getAuthor());
    update.set("enTitle", article.getEnTitle());
    mongoTemplate.updateMulti(query, update, Article.class);
    return true;
    } catch (Exception e) {
    log.info("update article error: " + e.toString());
    return false;
    }
    }
  5. 在Controller类中使用

    1
    2
    3
    4
    5
    6
    7
    8
    @GetMapping(value = "/news/{enTitle}")
    public String displaySingleNews(@PathVariable("enTitle") String enTitle, Model model, HttpSession session) {
    @Autowired
    private ArticleService articleService;
    Article findArticle = articleService.findByEnTitle(enTitle);
    model.addAttribute("news", findArticle);
    return "news/singleArticle";
    }

注意在代码中使用了@Resource注解,要理解@Resource和@Autowired的区别:

  • @Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入;
  • @Autowired默认是按照类型装配注入的,如果想按照名称来转配注入,则需要结合@Qualifier一起使用

文件流的处理

文件流的处理涉及到前端上传的方式和后台的处理方式,后台一般通过MultipartFile类型来处理文件流,这里使用Files的copy操作,查看copy的源码,其实它也是通过读写stream二进制流完成文件的写操作的。

1
2
3
4
5
6
7
8
public String store(MultipartFile file) {
String filename = StringUtils.cleanPath(file.getOriginalFilename());
try (InputStream inputStream = file.getInputStream()) {
Files.copy(inputStream, this.rootLocation.resolve(filename),
StandardCopyOption.REPLACE_EXISTING);
}
return this.rootLocation.resolve(filename).toString();
}

Files.copy中调用的私有copy方法源码

1
2
3
4
5
6
7
8
9
10
11
private static long copy(InputStream source, OutputStream sink)
throws IOException{
long nread = 0L;
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = source.read(buf)) > 0) {
sink.write(buf, 0, n);
nread += n;
}
return nread;
}

读文件的操作也是一样的,这里再关注一下程序是怎么接收文件的请求的。

1
2
3
4
5
6
@PostMapping("people/uploadNews")
public returnNewsStatus handleNewsUpload(MultipartHttpServletRequest httpServletRequest, HttpSession session) {
String uploadFilePath = "";
List<MultipartFile> fileList = httpServletRequest.getFiles("file");
String title = httpServletRequest.getParameter("newsTitle");
}

在上述代码中,我们使用MultipartHttpServletRequest类来接收请求的数据,这是为了处理包含了文件二进制流数据和正常文本数据的混合型请求(比如在发表文章时需要附带附件),而MultipartHttpServletRequest继承了HttpServletRequest, MultipartRequest类,这两个类可以分别处理二进制流数据和文本数据。

有时候有大文件上传的需求,只在application.properties中可以设置处理HTTP请求的大小限制,如下是把文件上传的限制改为50m大小。

1
2
spring.servlet.multipart.max-file-size=50MB
spring.servlet.multipart.max-request-size=50MB

踩坑经历:
之前文件下载是将其安排在resources的static目录下,这一安排的目的是使其作为静态资源,直接通过浏览器请求即可下载(如同对静态资源css、js这样的文件的处理),这种安排在开发环境是没有问题的,这是因为:静态资源只在项目启动的第一次进行加载,后续就不会实时加载改变的静态资源,但是在开发环境下,我们可以采用某些浏览器插件实现静态资源的实时更新。(参考 https://juejin.im/post/5d4cc6ac518825052c07e587 ),但是在生产环境就不能这么做了,因为用户是不会主动安装浏览器插件的,因此这种方案要pass掉,就要采用处理正常get请求一样处理文件下载的请求数据,该模块在GetFileHandle下实现。

使用docker部署项目

将项目通过docker-compose部署后的一个小技巧:

一旦将服务器主机和docker容器的目录映射之后,两者的文件内容是同步的,如果要修改项目文件内容,其实只要修改主机上的即可,volume会直接同步到容器。使用IDEA的docker plugin使得这一方式更加方便。这样的话即使要修改代码,只需要将代码使用scp命令同步一下即可。

记录Ubuntu开启docker 远程访问API

1
2
3
4
5
6
vim /lib/systemd/system/docker.service;
// 找到ExecStart,
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H
unix:///var/run/docker.sock;

systemctl daemon-reload

有一个想法:在本地开发完之后,能否直接把spring boot项目、mysql数据库、MongoDB打包在一个容器里,这样对于小型项目的部署会更加方便,而且可以使一套系统随意分发。(这个可以参考另一篇文章:docker系列之使用docker-compose部署 Spring Boot 项目

web设计安全及编码规范

具体编码过程中的一些细节

总结

当然,这些设计的内容,web开发的网站较为简单,内容交互和处理的方式还是比较简单,比如对于视频类的网站,该怎么处理,这里面涉及到二进制流数据的传输,视频传输一般都是建立在UDP的协议基础之上。还有对于淘宝这样的网站,后台的要求必然是非常之高的,主要是如何处理这么大的并发量,要考虑的细节问题之多超乎想象。
计算机领域有句话叫做:没有什么问题是一个中间件不能解决的,如果有,那就俩。因此面对各种需求问题,中间件的设计又处于一个非常重要的地位,比如阿里云举办的中间件大赛给中间件的设计提供了一个交流平台。